<?php 
/**
 * Sina Blog 7.0
 * @intro XSS Filter
 * @author quyang
 * @date 2010-01-28
 */
 
 /**
 * XSS Filter Class
 *
 * Pre-processes HTML data for security.
 * Note that the class need "tidy" extension be loaded,and UTF-8 charset only.
 */
Class XSSFilter {

    /**
     * XMLParser实例
     *
     * @var object
     */
    private $_parser;
    /**
     * Tidy扩展配置
     *
     * @var array
     */
    private $_tidy_config;
    /**
     * 直接干掉的标签
     *
     * @var array
     */
    private $_arrFilterTags = array('HTML', 'BODY', 'SCRIPT', 'META', 'XML', 'TITLE', 'HEAD', 'LINK', 'IFRAME');
    /**
     * 常见单体标签
     *
     * @var array
     */
    private $_arrSingleTag = array('IMG', 'HR', 'BGSOUND', 'FRAME', 'COL', 'BASE', 'BR', 'AREA', 'LINK', 'WBR', 'META', 'PARAM');
    
    /**
     * 解析错误信息
     *
     * @var array
     */
    private $_parse_error;
    
    /**
     * 处理中的HTML
     *
     * @var string
     */
    private $_HTML;
    
    /**
     * 处理中的标签
     *
     * @var string
     */
    private $_currentTag;
    
    /**
     * 构造函数
     *
     * @return void
     */
    public function XSSFilter() {
        $this->_initializeTidyExt();
        $this->_initializeParser();
    }
    /**
     * 过滤XSS(全套服务)
     *
     * @param string $string
     * @return string
     */
    public function cleanUpXSS($string) {
        if ( empty($string)) {
            return $string;
        }
        $string = $this->cleanBasicChar($string);
		//$string = $this->decodeHTMLEntities($string);
        $string = tidy_repair_string($string, $this->_tidy_config);
		$string = $this->protectHTMLEntities($string);
        xml_parse($this->_parser, $string, true);
        xml_parser_free($this->_parser);
        return $this->recoveryHTMLEntities($this->_HTML);
    }

    
    /**
     * 对输入的实体形式内容进行替换保护
     *
     * @param string $string
     * @return string
     */
    public function protectHTMLEntities($string) {
        return preg_replace("/&(#?[0-9a-zA-Z]{2,7});/", "__protect_\\1_word__", $string);
        
    }
    /**
     * 恢复被进行保护替换的实体形式的内容
     *
     * @param string $string
     * @return string
     */
    public function recoveryHTMLEntities($string) {
        return preg_replace("/__protect_(#?[0-9a-zA-Z]{2,7})_word__/U", "&\\1;", $string);
    }
    /**
     * 地址符形式字符解码
     *
     * @param string $string
     * @return string
     */
    public function decodeHTMLEntities($string) {
        // replace numeric entities
        $string = preg_replace('/&#x([0-9a-f]+);/ei', 'chr(hexdec("\\1"))', $string);
        $string = preg_replace('/&#([0-9]+);/e', 'chr(\\1)', $string);
        // replace literal entities
        $trans_tbl = get_html_translation_table(HTML_ENTITIES);
        $trans_tbl = array_flip($trans_tbl);
        return strtr($string, $trans_tbl);
    }
    /**
     * 高度危险的XSS关键字直接替换
     * 由于该类关键字可构成XSS的形式太广，考虑到正则效率问题进行直接替换
     *
     * @param string $string
     * @return string
     */
    public function highLevelDangerousCharEscape($string) {
        //For IE
        $regx = array("ｅ", "ｘ", "ｐ", "ｒ", "ｓ", "ｉ", "ｏ", "ｎ");
        $replace = array("e", "x", "p", "r", "s", "i", "o", "n");
        $string = str_replace($regx, $replace, $string);
        //For All
        $string = str_ireplace('expression', 'expressi0n', $string);
        $string = str_ireplace('eval', 'eva1', $string);
        return $string;
    }
    /**
     * 基本过滤
     *
     * @param string $string
     * @return string
     */
    public function cleanBasicChar($string) {
        $string = preg_replace("|\/\*(.*)\*\/|sU", "", $string);
        $string = preg_replace("/<!\[CDATA\[(.*?)\]\]>/is", "\\1", $string);
        return $string;
    }
    
    /**
     * 获取解析错误信息
     *
     * @return array
     */
    public function getParseError() {
        return $this->_parse_error = array('error_code'=>xml_get_error_code($this->_parser), 'error_string'=>xml_error_string(xml_get_error_code($this->_parser)), 'error_line'=>xml_get_current_line_number($this->_parser), );
    }
    
    /**
     * 处理标签开始
     *
     * @param object $parser
     * @param string $tagName
     * @param array $attrs
     * @return void
     */
    protected function _tagOpenHandler($parser, $tagName, $attrs) {
        $this->_currentTag = $tagName;
        if ($this->_filterNode($tagName)) {
            $this->_HTML .= "<".strtolower($tagName);
            foreach ($attrs as $name=>$value) {
                if ($this->_filterAttr($tagName, $name, $value)) {
                    $this->_HTML .= " ".strtolower($name).'="'.strtolower($value).'"';
                }
            }
            //加入对单标签的支持 如img
            if (in_array($tagName, $this->_arrSingleTag)) {
                $this->_HTML .= " />";
            } else {
                $this->_HTML .= ">";
            }
        }
    }
    
    /**
     * 处理标签结束
     *
     * @param object $parser
     * @param string $tagName
     * @return void
     */
    protected function _tagCloseHandler($parser, $tagName) {
        //去掉单体标签的结束标签 如<img>
        if ($this->_filterNode($tagName) && !in_array($tagName, $this->_arrSingleTag)) {
            $this->_HTML .= "</".strtolower($tagName).">";
        }
    }

	/**
	 * 处理标签间数据
	 * 
	 * @param object $parser
	 * @param string $data
	 * @return void
	 */
    protected function _cdataHandler($parser, $data) {
        if ($this->_filterNode($this->_currentTag) && !empty($data)) {
			$this->_HTML .= $data;
        }
    }
    
    /**
     * 判断该节点标签是否合法
     *
     * @param string $nodeName
     * @return bool
     */
    protected function _filterNode($nodeName) {
        if (in_array($nodeName, $this->_arrFilterTags)) {
            return false;
        }
        return true;
    }
    /**
     * 对节点属性进行过滤
     *
     * @param string $nodeName
     * @param string $attrName
     * @param string &$attrValue
     * @return string
     */
    protected function _filterAttr($nodeName, $attrName, &$attrValue) {
    	$attrName = strtoupper($attrName);
		$attrValue = strtoupper($attrValue);
        //干掉id和class属性
        if ($attrName == "ID" || $attrName == "CLASS") {
            return false;
        }
        //WMP漏洞容器漏洞OBJECT版
        if ($nodeName == "PARAM" || $nodeName == "EMBED") {
            if ($attrValue == "CAPTIONINGID" || $attrName == "CAPTIONINGID") {
                return false;
            }
        }
		//AllowScriptAccess设置为同域
		if ($nodeName == "PARAM"){
			if ($attrName == 'VALUE' && $attrValue == 'ALWAYS') {
                $attrValue = 'sameDomain';
            }
		}
		//类似iframe的实现
		if ($nodeName == "OBJECT" && $attrName == "DATA"){
			return false;
		}
        //如果过滤列表中制定了SCRIPT标签
        if (in_array('SCRIPT', $this->_arrFilterTags)) {
            if (substr($attrName, 0, 2) == 'ON') {
                return false;
            } 
			if (preg_match("/(javascript|mocha|livescript|vbscript|about|view-source|expression)/i", $attrValue)) {
                return false;
            } 
			if ($attrName == "STYLE") {
                $attrValue = $this->highLevelDangerousCharEscape($attrValue);
            }
        }
        return true;
    }
    /**
     * 初始化Tidy扩展配置
     *
     * @return void
     */
    protected function _initializeTidyExt() {
        tidy_set_encoding("UTF8");
        $this->_tidy_config = array('output-xhtml'=>true, 'show-body-only'=>false);
    }
    
    /**
     * 初始化解析器
     *
     * @return void
     */
    protected function _initializeParser() {
        $this->_parser = xml_parser_create('UTF-8');
        xml_set_object($this->_parser, $this);
        xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, true);
        xml_set_element_handler($this->_parser, '_tagOpenHandler', '_tagCloseHandler');
        xml_set_character_data_handler($this->_parser, '_cdataHandler');
    }
}
?>
